use actix_web::{web::Data, post, delete, put, get};
use actix_session::Session;
use serde::{Serialize, Deserialize};
use serde_json::json;
use validator::Validate;
use actix_web_validator::{Json, Query};
use sea_orm::{Set, EntityTrait, QueryFilter, ColumnTrait, ActiveModelTrait, QueryOrder, PaginatorTrait};

use crate::{controller::{resp, form::PagerForm}, model::user, util, AppState};

#[derive(Serialize, Deserialize)]
pub struct Auth {
    pub id: i32,
    pub username: String,
    pub role: i32
}

impl Auth {
    pub const LOGGED_IN: i32 = 1;
    pub const ADMIN: i32 = 2;

    pub fn require_role(session: &Session, min_role: i32) -> Result<Self, resp::Error> {
        session.get::<Auth>("auth")?
            .filter(|auth| auth.role >= min_role)
            .ok_or(resp::Error::new("Authentication failed"))
    }
}

#[derive(Deserialize, Validate)]
pub struct AuthForm {
    #[validate(length(min = 1, max = 30))]
    username: String,
    #[validate(length(min = 8, max = 30))]
    password: String
}

#[post("/user/auth")]
pub async fn login(data: Data<AppState>, session: Session, form: Json<AuthForm>) -> resp::Result {
    let user = user::Entity::find()
        .filter(user::Column::Username.eq(&form.username))
        .one(&data.db).await?;

    let user = match user {
        Some(user) if util::check_hash(&form.password, &user.password) => user,
        _ => return resp::err("Invalid username or password")
    };

    let auth = Auth {
        id: user.id,
        username: user.username,
        role: user.role
    };

    session.insert("auth", &auth)?;

    resp::ok(json!({ "user": auth }))
}

#[delete("/user/auth")]
pub async fn logout(session: Session) -> resp::Result {
    session.remove("auth");

    resp::ok(json!({}))
}

#[get("/user/auth")]
pub async fn get_login_status(session: Session) -> resp::Result {
    resp::ok(json!({ "user": session.get::<Auth>("auth")? }))
}

#[derive(Deserialize, Validate)]
pub struct PasswordForm {
    #[validate(length(min = 8, max = 30))]
    old_password: String,
    #[validate(length(min = 8, max = 30))]
    new_password: String
}

#[put("/user/password")]
pub async fn change_password(data: Data<AppState>, session: Session, form: Json<PasswordForm>) -> resp::Result {
    let auth = Auth::require_role(&session, Auth::LOGGED_IN)?;

    let user = user::Entity::find_by_id(auth.id)
        .one(&data.db).await?
        .ok_or(resp::Error::new("Current user does not exist"))?;

    if !util::check_hash(&form.old_password, &user.password) {
        return resp::err("Wrong old password");
    }

    let mut user: user::ActiveModel = user.into();
    user.password = Set(util::create_hash(&form.new_password)?);

    user.update(&data.db).await?;

    resp::ok(json!({}))
}

#[derive(Deserialize, Validate)]
pub struct UserForm {
    #[validate(length(min = 1, max = 30))]
    username: String,
    #[validate(length(min = 8, max = 30))]
    password: String,
    #[validate(range(min = 1, max = 2))]
    role: i32
}

#[post("/admin/user")]
pub async fn create_user(data: Data<AppState>, session: Session, form: Json<UserForm>) -> resp::Result {
    Auth::require_role(&session, Auth::ADMIN)?;

    let user = user::Entity::find()
        .filter(user::Column::Username.eq(&form.username))
        .one(&data.db).await?;

    if let Some(_) = user {
        return resp::err("Username already exists");
    }

    let form = form.into_inner();

    user::Entity::insert(user::ActiveModel {
        username: Set(form.username),
        password: Set(util::create_hash(&form.password)?),
        role: Set(form.role),
        created_at: Set(util::now()),
        ..Default::default()
    }).exec(&data.db).await?;

    resp::ok(json!({}))
}

#[get("/admin/user/list")]
pub async fn get_user_list(data: Data<AppState>, session: Session, form: Query<PagerForm>) -> resp::Result {
    Auth::require_role(&session, Auth::ADMIN)?;

    let total = user::Entity::find()
        .count(&data.db).await?;

    let user_list = user::Entity::find()
        .order_by_asc(user::Column::CreatedAt)
        .paginate(&data.db, form.limit)
        .fetch_page(form.page).await?;

    resp::ok(json!({ "total": total, "user_list": user_list }))
}
